package suit.persist;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Vector;

import javax.microedition.rms.InvalidRecordIDException;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import javax.microedition.rms.RecordStoreFullException;
import javax.microedition.rms.RecordStoreNotFoundException;
import javax.microedition.rms.RecordStoreNotOpenException;

import org.json.me.JSONException;
import org.json.me.JSONObject;

/**
 * Allows you to save JSONObjects for when the MIDlet is not running using RMS.
 * A lot of the functions are the very same as RecordStore class from JSR 118.
 * 
 * See PersistDemo.java for a good example.
 * 
 * @author Devin Samarin
 * 
 */
public class Persist {

	/**
	 * Authorization to allow access only to the current MIDlet suite
	 */
	public static final int PRIVATE = RecordStore.AUTHMODE_PRIVATE;
	/**
	 * Authorization to allow access to any MIDlet suite
	 */
	public static final int PUBLIC = RecordStore.AUTHMODE_ANY;

	private RecordStore store;
	private JSONObject object;
	private int jcurrentindex;

	private Vector listeners = new Vector();
	private boolean changed = true;

	/**
	 * Constructs a new Persist object. Similar to calling
	 * <code>new Persist("default", true)</code>
	 * 
	 * @exception PersistException
	 */
	public Persist() throws PersistException {
		this("default", true);
	}

	/**
	 * Constructor that opens (and possibly creates) a record store associated
	 * with the given MIDlet suite and initializes the JSONObject.
	 * 
	 * @param name
	 *            the MIDlet suite unique name for the record store, consisting
	 *            of between one and 32 Unicode characters inclusive.
	 * @param createIfNonexistant
	 *            if true, the record store will be created if it doesn't
	 *            already exist
	 * @exception PersistException
	 */
	public Persist(String name, boolean createIfNonexistant)
			throws PersistException {
		try {
			store = RecordStore.openRecordStore(name, createIfNonexistant);
			init();
		} catch (RecordStoreFullException e) {
			throw new PersistException("Record store full");
		} catch (RecordStoreNotFoundException e) {
			throw new PersistException("Record store not found");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}
	}

	/**
	 * Constructor that opens (and possibly creates) a record store that can be
	 * shared with other MIDlet suites and initializes the JSONObject. The
	 * RecordStore is owned by the current MIDlet suite. The authorization mode
	 * is set when the record store is created, as follows:
	 * 
	 * <ul>
	 * <li><code>PRIVATE</code> - Only allows the MIDlet suite that created the
	 * RecordStore to access it. This case behaves identically to
	 * <code>new Persist(name, createIfNotExists)</code>.</li>
	 * <li><code>PUBLIC</code> - Allows any MIDlet to access the information.
	 * Note that this makes your information accessible by any other MIDlet on
	 * the device. This could have privacy and security issues depending on the
	 * data being shared. Please use carefully.</li>
	 * </ul>
	 * 
	 * <p>
	 * The owning MIDlet suite may always access the information and always has
	 * access to write and update it.
	 * </p>
	 * 
	 * @param name
	 *            the MIDlet suite unique name for the record store, consisting
	 *            of between one and 32 Unicode characters inclusive.
	 * @param createIfNonexistant
	 *            if true, the record store will be created if necessary
	 * @param auth
	 *            the mode under which to check or create access. Must be one of
	 *            AUTHMODE_PRIVATE or AUTHMODE_ANY. This argument is ignored if
	 *            the RecordStore exists.
	 * @param writable
	 *            true if the RecordStore is to be writable by other MIDlet
	 *            suites that are granted access. This argument is ignored if
	 *            the RecordStore exists
	 * @exception PersistException
	 */
	public Persist(String name, boolean createIfNonexistant, int auth,
			boolean writable) throws PersistException {
		try {
			store = RecordStore.openRecordStore(name, createIfNonexistant,
					auth, writable);
			init();
		} catch (RecordStoreFullException e) {
			throw new PersistException("Record store full");
		} catch (RecordStoreNotFoundException e) {
			throw new PersistException("Record store not found");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}
	}

	/**
	 * Constructor that opens a record store associated with the named MIDlet
	 * suite. The MIDlet suite is identified by MIDlet vendor and MIDlet name.
	 * Access is granted only if the authorization mode allows access by the
	 * current MIDlet suite. Access is limited by the authorization mode set
	 * when the Persist object was created:
	 * 
	 * <ul>
	 * <li><code>PRIVATE</code> - Succeeds only if vendorName and suiteName
	 * identify the current MIDlet suite; this case behaves identically to
	 * <code>new Persist(name, createIfNotExistant)</code>.</li>
	 * <li><code>PUBLIC</code> - Always succeeds. Note that this makes your
	 * information accessible by any other MIDlet on the device. This could have
	 * privacy and security issues depending on the data being shared. Please
	 * use carefully. Untrusted MIDlet suites are allowed to share data but this
	 * is not recommended. The authenticity of the origin of untrusted MIDlet
	 * suites cannot be verified so shared data may be used unscrupulously.</li>
	 * </ul>
	 * 
	 * <p>
	 * If a MIDlet calls this method to open a record store from its own suite,
	 * the behavior is identical to calling:
	 * <code>{@link #Persist(String, boolean) new Persist(name, false)}</code>
	 * </p>
	 * 
	 * @param name
	 *            the MIDlet suite unique name for the record store, consisting
	 *            of between one and 32 Unicode characters inclusive.
	 * @param vendorName
	 *            the vendor of the owning MIDlet suite
	 * @param suiteName
	 *            the name of the MIDlet suite
	 * 
	 * @exception PersistException
	 */
	public Persist(String name, String vendorName, String suiteName)
			throws PersistException {
		try {
			store = RecordStore.openRecordStore(name, vendorName, suiteName);
			init();
		} catch (RecordStoreNotFoundException e) {
			throw new PersistException("Record store not found");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}
	}

	public static void delete(String name) throws PersistException {
		try {
			RecordStore.deleteRecordStore(name);
		} catch (RecordStoreNotFoundException e) {
			throw new PersistException("Record store can not be found.");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception.");
		}
	}

	private void init() throws PersistException, RecordStoreNotOpenException,
			RecordStoreFullException, RecordStoreException {
		if (store.getNumRecords() == 0) {
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			DataOutputStream dout = new DataOutputStream(bout);
			try {
				dout.writeUTF("{}");
				dout.close();
			} catch (IOException e) {
			}
			byte[] data = bout.toByteArray();
			jcurrentindex = store.addRecord(data, 0, data.length);
		} else {
			jcurrentindex = 1;
		}
	}

	public static void list() {
		RecordStore.listRecordStores();
	}

	/**
	 * Changes the access mode for this RecordStore. The authorization mode
	 * choices are:
	 * 
	 * <ul>
	 * <li><code>PRIVATE</code> - Only allows the current MIDlet suite to access
	 * it. This case behaves identically to <code>new Persist(name,
	 *          createIfNonExistant)</code>.</li>
	 * <li><code>PUBLIC</code> - Allows any MIDlet to access the information.
	 * Note that this makes your recordStore accessible by any other MIDlet on
	 * the device. This could have privacy and security issues depending on the
	 * data being shared. Please use carefully.</li>
	 * </ul>
	 * 
	 * <p>
	 * The owning MIDlet suite may always access the information and always has
	 * access to write and update it. Only the owning MIDlet suite can change
	 * the mode of a Persist object.
	 * </p>
	 * 
	 * @param authmode
	 *            the mode under which to check or create access. Must be one of
	 *            PRIVATE or PUBLIC.
	 * @param writable
	 *            true if the RecordStore is to be writable by other MIDlet
	 *            suites that are granted access
	 * 
	 * @exception RecordStoreException
	 *                if a record store-related exception occurred
	 * @exception SecurityException
	 *                if this MIDlet Suite is not allowed to change the mode of
	 *                the RecordStore
	 * @exception IllegalArgumentException
	 *                if authmode is invalid
	 */
	public void setMode(int authmode, boolean writable)
			throws RecordStoreException, SecurityException,
			IllegalArgumentException {
		store.setMode(authmode, writable);
	}

	/**
	 * This method is called when the MIDlet requests to have the record store
	 * closed. Note that the record store will not actually be closed until
	 * close() is called as many times as the constructor is created with the
	 * same record store name. In other words, the MIDlet needs to make a
	 * balanced number of close calls as constructors before the record store is
	 * closed.
	 * 
	 * <p>
	 * When this method is called, all listeners are removed. If the MIDlet
	 * attempts to perform operations on the object after it has been closed,
	 * the methods will throw a RecordStoreNotOpenException or PersistException.
	 * 
	 * @exception PersistException
	 *                if the record store was not open in the first place
	 */
	public void close() throws PersistException {
		try {
			store.closeRecordStore();
		} catch (RecordStoreNotOpenException e) {
			throw new PersistException("Record store not open");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}

	}

	/**
	 * Returns the name of this Persist object.
	 * 
	 * @return the name of this Persist object
	 * @exception RecordStoreNotOpenException
	 *                if the record store is not open
	 */
	public String getName() throws RecordStoreNotOpenException {
		return store.getName();
	}

	/**
	 * Each time the persist is modified (by the <code>flush</code> method) its
	 * <em>version</em> is incremented. This can be used by MIDlets to quickly
	 * tell if anything has been modified. You can also add a listener with
	 * <code>addListener</code>, but this will obviously not be called if the
	 * MIDlet is not currently running.
	 * 
	 * The initial version number is implementation dependent. The increment is
	 * a positive integer greater than 0. The version number increases only when
	 * the RecordStore is updated.
	 * 
	 * The increment value need not be constant and may vary with each update.
	 * 
	 * @return the current record store version
	 * @exception RecordStoreNotOpenException
	 *                if the record store is not open
	 */
	public int getVersion() throws RecordStoreNotOpenException {
		return store.getVersion();
	}

	/**
	 * Returns the amount of space, in bytes, that the record store occupies.
	 * The size returned includes any overhead associated with the
	 * implementation, such as the data structures used to hold the state of the
	 * record store, etc.
	 * 
	 * @exception RecordStoreNotOpenException
	 *                if the record store is not open
	 * @return the size of the record store in bytes
	 */
	public int getRecordSize() throws RecordStoreNotOpenException {
		return store.getSize();
	}

	/**
	 * Returns the amount of additional room (in bytes) available for this
	 * record store to grow. Note that this is not necessarily the amount of
	 * extra MIDlet-level data which can be stored, as implementations may store
	 * additional data structures with each record to support integration with
	 * native applications, synchronization, etc.
	 * 
	 * @exception RecordStoreNotOpenException
	 *                if the record store is not open
	 * 
	 * @return the amount of additional room (in bytes) available for this
	 *         record store to grow
	 */
	public int getSizeAvailable() throws RecordStoreNotOpenException {
		return store.getSizeAvailable();
	}

	/**
	 * Returns the last time the record store was modified, in the format used
	 * by System.currentTimeMillis().
	 * 
	 * @return the last time the record store was modified, in the format used
	 *         by System.currentTimeMillis()
	 * 
	 * @exception RecordStoreNotOpenException
	 *                if the record store is not open
	 */
	public long getLastModified() throws RecordStoreNotOpenException {
		return store.getLastModified();
	}

	/**
	 * Adds the specified PersistListener. If the specified listener is already
	 * registered, it will not be added a second time. When <code>close()</code>
	 * is called, all listeners are removed.
	 * 
	 * @param listener
	 *            the RecordChangedListener
	 * @see #removeListener
	 */
	public void addListener(PersistListener listener) {
		if (listeners == null)
			listeners = new Vector();
		if (!listeners.contains(listener))
			listeners.addElement(listener);
	}

	/**
	 * Removes the specified PersistListener. If the specified listener is not
	 * registered, this method does nothing.
	 * 
	 * @param listener
	 *            the PersistListener
	 * @see #addListener
	 */
	public void removeListener(PersistListener listener) {
		if (listeners != null)
			listeners.removeElement(listener);
	}

	/**
	 * Returns the size (in bytes) of the JSONObject.
	 * 
	 * @return the size (in bytes) of the JSONObject
	 * 
	 * @exception PersistException
	 *                if the record store is not open
	 */
	public int getSize() throws PersistException {
		int ret = 0;
		try {
			ret = store.getRecordSize(jcurrentindex);
		} catch (RecordStoreNotOpenException e) {
			throw new PersistException("Record store not open");
		} catch (InvalidRecordIDException e) {
			throw new PersistException("Invalid record id");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}
		return ret;
	}

	/**
	 * Return the JSONObject which you can modify. Later you can call
	 * <code>flush()</code> to save changes.
	 * 
	 * @return the JSONObject
	 * @throws PersistException
	 *             if the record store is not open, or was not created by a
	 *             Persist object.
	 */
	public JSONObject get() throws PersistException {
		if (changed) {
			try {
				ByteArrayInputStream bin = new ByteArrayInputStream(store
						.getRecord(jcurrentindex));
				DataInputStream din = new DataInputStream(bin);
				object = new JSONObject(din.readUTF());
				din.close();
				changed = false;
			} catch (RecordStoreNotOpenException e) {
				throw new PersistException("Record store not open");
			} catch (InvalidRecordIDException e) {
				throw new PersistException("Invalid record id -- this is a bug");
			} catch (RecordStoreException e) {
				throw new PersistException("Record store exception");
			} catch (JSONException e) {
				throw new PersistException("Not a valid JSON string");
			} catch (IOException e) {
				throw new PersistException("Can't read from record store");
			}
		}
		return object;
	}

	/**
	 * Set the JSONObject to use, and flushes it to the record store.
	 * 
	 * @param object
	 *            the JSONObject
	 * @throws PersistException
	 *             if any one of a number of issues occurs
	 */
	public void set(JSONObject object) throws PersistException {
		this.object = object;
		changed = false;
		flush();
	}

	/**
	 * Flush the current JSONObject to the record store. It is good to call this
	 * before your program exits, so your changes are saved for the next time
	 * around. If your program exits without calling this method, then changes
	 * will be lost.
	 * 
	 * @throws PersistException
	 *             if any one of a number of issues occurs
	 */
	public void flush() throws PersistException {
		try {
			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			DataOutputStream dout = new DataOutputStream(bout);
			dout.writeUTF(object.toString());
			dout.close();
			byte[] data = bout.toByteArray();
			store.setRecord(jcurrentindex, data, 0, data.length);
		} catch (IOException e) {
			throw new PersistException("Can't write to record store");
		} catch (RecordStoreNotOpenException e) {
			throw new PersistException("Record store not open");
		} catch (InvalidRecordIDException e) {
			throw new PersistException("Invalid record id -- this is a bug");
		} catch (RecordStoreFullException e) {
			throw new PersistException("Record store full");
		} catch (RecordStoreException e) {
			throw new PersistException("Record store exception");
		}
		changed();
	}

	private void changed() {
		int size = listeners.size();
		if (size == 0)
			return;
		for (int i = 0; i < size; i++) {
			((PersistListener) listeners.elementAt(i)).onChanged(this);
		}
	}
}
